package org.example.common.session;

import cn.hutool.core.lang.Pair;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.interfaces.Claim;
import com.google.common.base.Throwables;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.extern.slf4j.Slf4j;
import org.example.common.configs.exception.ServiceException;
import org.example.common.enums.WGErrorEnum;
import org.example.model.dto.FullUserDto;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * @author weigang
 * @create 2022-11-08 14:54
 */
@Slf4j
public class DefaultSessionStore implements SessionStore{
    private Long tokenExpireMillis;
    private final UserLoader userLoader;

    public DefaultSessionStore(Long tokenExpireMillis, UserLoader userLoader) {
        this.tokenExpireMillis = tokenExpireMillis;
        this.userLoader = userLoader;
    }

    /**
     *  应该再有一个数据结构，维护it code和token的关系，判断it code是否已经登录
     */

    // 缓存的加载
    // 半持久化k-v
    // 线程安全。语义 ：“get-if-absent-compute” 获取缓存-如果没有-则计算。lock加锁，原子操作。
    // https://zhuanlan.zhihu.com/p/566880204
    private final LoadingCache<String, FullUserDto> userCache = CacheBuilder.newBuilder()
            // 设定最大值
            // 达到存储容量后，自动回收。使用【最近最少使用LRU算法】
            .maximumSize(1000)
            // refresh 只阻塞加载数据的线程，其余线程返回旧数据
            // 指定时间后，不会remove该key，而是在下次访问触发刷新
            // 如果使用refreshAfterWrite，在吞吐量很低的情况下，如很长一段时间内没有查询之后，
            // 发生的查询有可能会得到一个旧值（这个旧值可能来自于很长时间之前，并且refresh）
            .refreshAfterWrite(1L, TimeUnit.HOURS)
            // 每次更新之后的指定时间让缓存失效
            // 再次访问触发重新加载，加锁，其他请求必须阻塞等待这个加载操作完成；
            // 而且，在加载完成之后，其他请求的线程会逐一获得锁，去判断是否已被加载完成，
            // 每个线程必须轮流地走一个“”获得锁，获得值，释放锁“”的过程，这样性能会有一些损耗
            .expireAfterWrite(1L, TimeUnit.DAYS)
            // 在refreshAfterWrite和expireAfterWrite找到一个折中
            // 访问频繁的时候，会在每个小时都进行refresh，而当超过1天没有访问，下一次访问必须load新值（或者缓冲失效，这里就是会token失效）
            .build(new CacheLoader<String, FullUserDto>() {
                @Override
                // 这个 load 的方法的作用是：
                // 如果我们调用 LoadingCache 中的 get 方法，缓存不存在相对应的 key 的数据，
                // 那么 CacheLoader 会自动调用 load 方法加载数据进来，
                // 至于数据从哪里来是你自己设计的，比如从数据库或者 Redis 等等。
                // 也就是说，查询时key不存在，会调用load方法，加载数据存储到缓存中
                public FullUserDto load(String token) throws Exception {
                    log.info(" >>>>> userCache is no exist token : [{}] <<<<< " , token);
                    Claim itCodeClaim = TokenUtil.getClaim(token, TokenUtil.JWT_KEY_IT_CODE);
                    if (itCodeClaim.isNull()) {
                        log.error("DefaultSessionStore load token it code null, token : [{}]", token);
                        throw new ServiceException(WGErrorEnum.e_1022.getCode(), WGErrorEnum.e_1022.getMsg());
                    }
                    // 在JwtSessionConfig中已经通过bean进行有参构造函数实例化了DefaultSessionStore
                    // userLoader 是一个function接口
                    log.info(" >>>>> start loading form Database , IT code : [{}] <<<<< " , itCodeClaim.asString());
                    return userLoader.loadUser(itCodeClaim.asString());
                }
            });

    @Override
    public Pair<String, FullUserDto> getUser(String token, String itCode) {
        log.info("itcode : [{}] , token : [{}]" , itCode , token);

        if (CharSequenceUtil.isEmpty(token)) {
            if (CharSequenceUtil.isEmpty(itCode)) {
                throw new ServiceException(WGErrorEnum.e_1024.getCode(), WGErrorEnum.e_1024.getMsg());
            }
            token = TokenUtil.generateToken(itCode);
            log.info("itcode : [{}] , generate token : [{}]" , itCode , token);
        }

        try {
            // get(key) 时，如果缓存不存在相对应的 key 的数据，
            // 那么 CacheLoader 会自动调用 配置的load 方法加载数据进来。至于怎么加载，load方法内容是自己构建的
            return new Pair<>(token, userCache.get(token));
        } catch (ExecutionException e) {
            log.error("DefaultSessionStore getUser failed, token : [{}], user : [{}], err : [{}]", token, itCode, Throwables.getStackTraceAsString(e));
            Throwable cause = e.getCause();
            if (cause instanceof ServiceException) {
                throw (ServiceException) cause;
            } else {
                throw new ServiceException(WGErrorEnum.e_1020.getCode(), WGErrorEnum.e_1020.getMsg());
            }
        }
    }

    @Override
    public Pair<String, FullUserDto> getIfPresent(String token) {
        if (StrUtil.isEmpty(token)) {
            throw new ServiceException(WGErrorEnum.e_1023.getCode(), WGErrorEnum.e_1023.getMsg());
        }

        // userCache.getIfPresent 从现有的缓存中获取，如果缓存中有key，则返回value，如果没有则返回null
        FullUserDto ifPresent = userCache.getIfPresent(token);
        return new Pair<>(token, userCache.getIfPresent(token));
    }

    // 手动刷新
    // 刷新即不从cache中取，实际调用的是load方法从定义的内容中取，然后再放到缓存中
    // 中间会穿插一些 加锁解锁，判断是否过期 等处理。
    @Override
    public void refresh(String token) {
        userCache.refresh(token);
    }

    // invalidate() 手动清除token，单个清除
    // 在用户手动登出时进行服务端token清除
    @Override
    public void clearUser(String token) {
        userCache.invalidate(token);
    }
}
